package ch.zhaw.cctd.logic.xml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import ch.zhaw.cctd.domain.prototype.CreepPrototype;
import ch.zhaw.cctd.domain.prototype.MapPrototype;
import ch.zhaw.cctd.domain.prototype.Prototype;
import ch.zhaw.cctd.domain.prototype.PrototypeList;
import ch.zhaw.cctd.domain.prototype.TowerPrototype;
import ch.zhaw.cctd.domain.prototype.TowerUpgradePrototype;
import ch.zhaw.cctd.domain.prototype.WavePrototype;

/**
 * Liest ein CCTD-XML-File und wandelt die vorhandenen Daten in Prototypes um.
 * @author Benjamin Felder
 */
public final class XmlConfigReader {
	
	private static final Logger logger = LoggerFactory.getLogger(XmlConfigReader.class);
	
	/**
	 * Read data from a file.
	 * @param filename	path to a file
	 * @see #readData(File)
	 */
	public static final PrototypeList readData(final String filename) 
		throws XmlReadException, FileNotFoundException
	{
		return XmlConfigReader.readData(new File(filename));
	}
	
	/**
	 * Read data from a file.
	 * @param file	the file object representation of a file.
	 * @see #readData(InputStream)
	 */
	public static final PrototypeList readData(final File file) 
		throws XmlReadException, FileNotFoundException
	{
		return XmlConfigReader.readData(new FileInputStream(file));
	}
	
	/**
	 * Read data from a stream.
	 * @param stream	a InputStream
	 * @return	a list of all generated <code>Prototype</code>s
	 * @throws XmlReadException	if some error occurred during reading or parsing
	 */
	public static final PrototypeList readData(final InputStream stream) 
		throws XmlReadException 
	{
		return XmlConfigReader.getInstance().readDataInternal(stream);
	}
	
	private static final XmlConfigReader reader = new XmlConfigReader();
	private static final XmlConfigReader getInstance() {
		return reader;
	}
	
	/*
	 * the actual parsing and reading of the data 
	 * @param stream	the input-stream do read the data from.
	 * @return		the list of all generated prototypes
	 * @throws XmlReadException
	 */
	private final PrototypeList readDataInternal(final InputStream stream)
		throws XmlReadException 
	{
		logger.trace("reading data from inputstream");
		Element root = this.getRootElement(stream);
		
		String version = root.getAttribute("version");
		if (Double.parseDouble(version) != 1)
			throw new UnsupportedVersionException(version);
		
		NodeList elements = root.getChildNodes();
		List<Prototype> data = new ArrayList<Prototype>();
		for (XmlNodeListIterator it = new XmlNodeListIterator(elements);it.hasNext();) {
			Node node = it.next();
			String nodename = node.getNodeName();
			
			String methodName = "handle"+nodename.toUpperCase().charAt(0)+nodename.substring(1);
			try {
				Method handleMethod = this.getClass().getDeclaredMethod(methodName, Node.class);
				@SuppressWarnings("unchecked")
				List<Prototype> gendata = (List<Prototype>)handleMethod.invoke(this, node);
				data.addAll(gendata);
			} catch (Exception e) {
				logger.error("error occured during introspection: ", e);
				throw new XmlReadError(e);
			}
		}
		PrototypeList list = new PrototypeList(data);
		return list;
	}
	
	/*
	 * Initialises the parser.
	 */
	private final Element getRootElement(final InputStream stream) 
		throws XmlReadException 
	{
		DocumentBuilder db;
		try {
			db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			Document d = db.parse(stream);
			return d.getDocumentElement();
		} catch (ParserConfigurationException e) {
			throw new XmlReadError(e);
		} catch (Exception e) {
			throw new XmlReadException(e);
		}
	}
	
	private final Map<String,String> subhandleChilds(Map<String,String> data, Node node) {
		return this.subhandleChilds(data, node, "");
	}
	
	/*
	 * recursive generic method to parse the a subtree of the xml
	 */
	private final Map<String,String> subhandleChilds(Map<String,String> data, Node node, String prefix) {
		logger.debug("reading data from node {}  with prefix: {}", node.getNodeName(), prefix);
		for (XmlNodeListIterator it = new XmlNodeListIterator(node.getChildNodes());it.hasNext();) {
			Node child = it.next();
			String newprefix = new StringBuffer(prefix).append(child.getNodeName()).append(".").toString();
			if (child.hasChildNodes()) {
				if (
						child.getChildNodes().getLength() == 1 &&
						child.getChildNodes().item(0).getNodeType() == Node.TEXT_NODE
				) {
					final String key = newprefix+"#text";
					final String value = child.getChildNodes().item(0).getNodeValue();
					data.put(key, value);
					logger.trace("adding {}#text->", key, value);
				} else {
					this.subhandleChilds(data, child, newprefix);
				}
			}
			if (child.hasAttributes()) {
				NamedNodeMap attributes = child.getAttributes();
				if (attributes.getLength() > 1) {
					// if there are >1 attributes, a index must be generated because a simple list isn't enough
					String counterKey = newprefix.substring(0, newprefix.length()-1);
					int counter = (data.containsKey(counterKey)) ? Integer.parseInt(data.get(counterKey)):0;
					newprefix = new StringBuffer(counterKey).append(".").append(counter).append(".").toString();
					counter++;
					data.put(counterKey, ""+counter);
					logger.trace("adding {}->{}", counterKey, counter);
				}
				for (XmlNamedNodeMapIterator attIt = new XmlNamedNodeMapIterator(attributes);attIt.hasNext();) {
					Node att = attIt.next();
					String key = newprefix + att.getNodeName();
					String content = att.getNodeValue();
					if (data.containsKey(key)) {
						content = data.get(key)+";"+att.getNodeValue();
					}
					data.put(key, content);
					logger.trace("adding {}->{}", key, content);
				}
			}
		}
		return data;
	}
	
	/*
	 * intermediate parsing block for attributes of a node.
	 */
	private final Map<String,String> subhandleAttributes(Map<String,String> data, NamedNodeMap attributes) {
		for (Iterator<Node> it = new XmlNamedNodeMapIterator(attributes);it.hasNext();) {
			Node node = it.next();
			data.put(node.getNodeName(), node.getNodeValue());
			logger.trace("adding {}->{}", node.getNodeName(), node.getNodeValue());
		}
		return data;
	}
	
	/*
	 * parses the <towers> section
	 */
	@SuppressWarnings("unused")
	private final List<Prototype> handleTowers(final Node node) {
		final NodeList nodes = node.getChildNodes();
		logger.trace("handling towers");
		List<Prototype> data = new ArrayList<Prototype>();
		for (XmlNodeListIterator it = new XmlNodeListIterator(nodes);it.hasNext();) {
			logger.trace("found a tower:");
			Node towerNode = it.next();
			Map<String,String> tower = new HashMap<String,String>();
			// parse attributes
			NamedNodeMap attributes = towerNode.getAttributes();
			tower = this.subhandleAttributes(tower, towerNode.getAttributes());
			// parse upgrades
			NodeList towerChilds = towerNode.getChildNodes();
			List<TowerUpgradePrototype> upgrades = null;
			for (XmlNodeListIterator childIt = new XmlNodeListIterator(towerChilds);childIt.hasNext();) {
				Node child = childIt.next();
				if ("upgrades".equals(child.getNodeName())) {
					upgrades = this.subhandleTowerUpgrades(child.getChildNodes());
					break;
				}
			}
			// parse rest
			data.add(new TowerPrototype(this.subhandleChilds(tower, towerNode), upgrades));
		}
		return data;
	}
	
	/*
	 * parses the <upgrades> subsection of the <towers> section.
	 */
	private final List<TowerUpgradePrototype> subhandleTowerUpgrades(final NodeList nodes) {
		logger.trace("adding upgrades to tower");
		List<TowerUpgradePrototype> data = new ArrayList<TowerUpgradePrototype>();
		for (XmlNodeListIterator it = new XmlNodeListIterator(nodes);it.hasNext();) {
			Node upgradeNode = it.next();
			Map<String,String> upgrade = new HashMap<String,String>();
			logger.trace("adding upgrade to tower:");
			// parse attributes
			upgrade = this.subhandleAttributes(upgrade, upgradeNode.getAttributes());
			// parse childs
			data.add(new TowerUpgradePrototype(this.subhandleChilds(upgrade, upgradeNode)));
		}
		return data;
	}
	
	/*
	 * parses the <creeps> section.
	 */
	@SuppressWarnings("unused")
	private final List<Prototype> handleCreeps(final Node node) {
		final NodeList nodes = node.getChildNodes();
		logger.trace("handling creeps");
		List<Prototype> data = new ArrayList<Prototype>();
		for (XmlNodeListIterator it = new XmlNodeListIterator(nodes);it.hasNext();) {
			Node creepNode = it.next();
			logger.trace("adding creep:");
			Map<String,String> creep = new HashMap<String,String>();
			// parse attributes
			creep = this.subhandleAttributes(creep, creepNode.getAttributes());
			// parse childs
			data.add(new CreepPrototype(this.subhandleChilds(creep, creepNode)));
		}
		return data;
	}
	
	/*
	 * parses the <waves> section
	 */
	@SuppressWarnings("unused")
	private final List<Prototype> handleWaves(final Node node) {
		final NodeList nodes = node.getChildNodes();
		logger.trace("handling waves");
		List<Prototype> data = new ArrayList<Prototype>();
		for (XmlNodeListIterator it = new XmlNodeListIterator(nodes);it.hasNext();) {
			Node waveNode = it.next();
			logger.trace("adding wave:");
			Map<String,String> wave = new HashMap<String,String>();
			// parse attributes
			wave = this.subhandleAttributes(wave, waveNode.getAttributes());
			// parse childs
			data.add(new WavePrototype(this.subhandleChilds(wave, waveNode)));
		}
		return data;
	}
	
	/*
	 * parses the <map> section
	 */
	@SuppressWarnings("unused")
	private final List<Prototype> handleMap(final Node node) {
		final NodeList nodes = node.getChildNodes();
		logger.trace("handling map");
		List<Prototype> data = new ArrayList<Prototype>();
		MapPrototype mapPrototype = new MapPrototype(this.subhandleChilds(new HashMap<String,String>(), node));
		data.add(mapPrototype);
		data.addAll(mapPrototype.getMapTexturePrototypes());
		data.addAll(mapPrototype.getSpawnPointPrototypes());
		data.addAll(mapPrototype.getWayPointsPrototypes());
		return data;
	}
	
}
